/* Calculation.cs - Hier wird der Projekt-/Task-/Phasen-Plan aus den Calculation-Daten berechnet
 * Copyright (C) 2009 Dietmar Schinnerl
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System;

namespace App.Net
{
    public class Calculation
    {
        public List<CalcTask> TaskList;
        public List<CalcWorker> WorkerList;
        public AppProject AppProject;
        public int[, ,] WeeklyAssignment;
        public int[, ,] PhaseAssignment;
        public int Phases;
        public PhaseMetadata[] PhaseMdList;
        public int BestError;
        public BestResult BestResult;
        public int Iteration;
        public const int BestErrorStartValue = 99999999;
        private int _maxWeeks;
        private ArrayList _alreadyChosen;
        private Random _random;
        private BackgroundWorker _backgroundWorker;
 


        public Calculation(AppProject appProject, BackgroundWorker backgroundWorker)
        {
            TaskList = new List<CalcTask>();
            WorkerList = new List<CalcWorker>();
            BestResult = new BestResult();
            this.AppProject = appProject;
            Init(appProject.TaskFolderTree, appProject.WorkerFolderTree);
            //maxWeeks = 250;
            _maxWeeks = 250 * 7;
            _alreadyChosen = new ArrayList();
            _random = new Random((int)DateTime.Now.Ticks);
            this._backgroundWorker = backgroundWorker;
        }


        public void Calculate()
        {
            BestError = BestErrorStartValue;
            WeeklyAssignment = new int[WorkerList.Count, TaskList.Count + 1, _maxWeeks];
            PhaseAssignment = new int[WorkerList.Count, TaskList.Count + 1, TaskList.Count];
            PhaseMdList = new PhaseMetadata[TaskList.Count];

            if (AppProject.CalculationPeriod == AppProject.Period.Weekly)
                CalculateWeeklyHoursPerWorker();
            else
                CalculateDailyHoursPerWorker();
            Iteration = 0;

        }

        /// 
        /// <param name="nrOfIterations"></param>
        public void CalculateIterations(int nrOfIterations)
        {
            #region ITERATIONS
            for (int it = 0; it < nrOfIterations; it++)
            {
                Iteration++;
                #region INITIALIZATION per ITERATION
                int phase = 0;
                int lastWeekWorking = 0;
                //DateTime weekDate = appProject.StartDate;
                DateTime weekDate = AppProject.TodayDate.DateTime;
                int lastPhaseWeek = 0;
                //DateTime lastPhaseDate = appProject.StartDate;
                DateTime lastPhaseDate = AppProject.TodayDate.DateTime;

                int nrOfTasksFinishedInThisIteration = 0;

                InitIteration();


                #endregion


                #region WEEKS
                int week = 0;
                for (week = 0; week < _maxWeeks; week++)
                {
                    if (nrOfTasksFinishedInThisIteration == TaskList.Count)
                        break;
                    bool aTaskFinishedThisWeek = false;

                    #region WORKERS
                    for (int workerIndex = 0; workerIndex < WorkerList.Count; workerIndex++)
                    {
                        CalcWorker cWorker = (CalcWorker)WorkerList[workerIndex];
                        int workersHoursThisWeek = WeeklyAssignment[workerIndex, TaskList.Count, week];
                        int workersHoursThisWeekForSelectedTask = 0;
                        int chosenTaskIndex = -1;
                        _alreadyChosen.Clear();
                        do
                        {
                            bool relationOk = false;
                            CalcSkilledWorker calcSkilledWorker = null;
                            do
                            {
                                chosenTaskIndex = ChooseTask();
                                if (chosenTaskIndex == -1)
                                    continue;
                                CalcTask chosenTask = (CalcTask)TaskList[chosenTaskIndex];


                                workersHoursThisWeekForSelectedTask = workersHoursThisWeek;


                                if (_alreadyChosen.Contains(chosenTaskIndex))
                                    continue;

                                if (chosenTask.RemainingHours < workersHoursThisWeekForSelectedTask)
                                    workersHoursThisWeekForSelectedTask = chosenTask.RemainingHours;


                                _alreadyChosen.Add(chosenTaskIndex);

                                //if (chosenTask.remainingHours <= 0) continue;

                                #region FINISHWITH TASK

                                if (chosenTask.FinishWithCalcTask != null)
                                {
                                    if (chosenTask.Percentage() > chosenTask.FinishWithCalcTask.Percentage())
                                        continue;
                                    else
                                    {
                                        if (((AppProject.CalculationPeriod == AppProject.Period.Weekly)
                                            && (chosenTask.Percentage() > chosenTask.FinishWithCalcTask.Percentage() - 20))
                                            ||
                                            ((AppProject.CalculationPeriod == AppProject.Period.Daily)
                                            && (chosenTask.Percentage() > chosenTask.FinishWithCalcTask.Percentage() - 5)))

                                            workersHoursThisWeekForSelectedTask = _random.Next(0, workersHoursThisWeek); // ein kleiner zufallsfaktor damit "finish mit task" sich auch genau ausgehen kann
                                    }
                                }

                                #endregion


                                if (chosenTask.RemainingHours <= 0) // Task already finished
                                    continue;

                                if (chosenTask.Task.EarliestStartDate.DateTime.Date > weekDate.Date) // Earliest Start Date not reached
                                    continue;

                                #region CHECK PREDECESSORS

                                bool onePredecessorNotFinished = false;
                                foreach (CalcPredecessor calcPredecessor in chosenTask.CalcPredecessorList)
                                {
                                    if (calcPredecessor.CalcTask.Percentage() < calcPredecessor.Predecessor.Percentage)
                                        onePredecessorNotFinished = true;
                                }
                                if (onePredecessorNotFinished == true)
                                    continue;

                                #endregion

                                foreach (CalcSkilledWorker csw in chosenTask.CalcSkilledWorkerList)
                                {
                                    if (csw.SkilledWorker.WorkerId == cWorker.Worker.Id)
                                    {
                                        if ((csw.SkilledWorker.MaxPercentageOfTask == 100) || (csw.Percentage() < csw.SkilledWorker.MaxPercentageOfTask)) // max percentage of task per worker reached 
                                        {
                                            calcSkilledWorker = csw;
                                            relationOk = true;
                                        }
                                    }
                                }
                            }
                            while ((!relationOk) && (_alreadyChosen.Count < TaskList.Count));

                            #region A TASK IS CHOSEN FOR THIS WORKER IN THIS WEEK
                            if (relationOk) // A task is chosen for worker in this week
                            {
                                lastWeekWorking = week;

                                #region ADD HOURS TO SKILLED WORKER
                                // if training time reached 
                                bool training = false;
                                if (calcSkilledWorker.HoursWorkedOnTrainingTime >= calcSkilledWorker.SkilledWorker.TrainingTime)
                                    calcSkilledWorker.HoursWorkedOnTask += workersHoursThisWeekForSelectedTask;
                                else
                                {
                                    training = true;
                                    calcSkilledWorker.HoursWorkedOnTrainingTime += workersHoursThisWeekForSelectedTask;
                                }
                                #endregion

                                #region ASSIGN HOURS TO TASK


                                if (!training)
                                    if (workersHoursThisWeekForSelectedTask < 5)
                                        WeeklyAssignment[workerIndex, chosenTaskIndex, week] = workersHoursThisWeekForSelectedTask;
                                    else
                                    {
                                        int maxHours = ((int)(long)((workersHoursThisWeekForSelectedTask * calcSkilledWorker.SkilledWorker.Productivity * calcSkilledWorker.SkilledWorker.MaxPercentagePerWeek) / 10000));
                                        if (workersHoursThisWeekForSelectedTask > maxHours)
                                            workersHoursThisWeekForSelectedTask = maxHours;
                                        WeeklyAssignment[workerIndex, chosenTaskIndex, week] = workersHoursThisWeekForSelectedTask;
                                    }
                                #endregion
                                workersHoursThisWeek -= workersHoursThisWeekForSelectedTask;


                            }
                            #endregion

                        } while ((workersHoursThisWeek > 0) && (_alreadyChosen.Count < TaskList.Count));


                    }
                    #endregion

                    #region REMOVE HOURS IN TASK / If task finished this week change Phase Meta Data
                    // The reason for why I do this here is a correcnt predecessor calculation
                    for (int taskIndex = 0; taskIndex < TaskList.Count; taskIndex++)
                    {
                        CalcTask calcTask = (CalcTask)TaskList[taskIndex];
                        if (calcTask.RemainingHours > 0)
                        {
                            for (int workerIndex = 0; workerIndex < WorkerList.Count; workerIndex++)
                            {
                                calcTask.RemainingHours -= WeeklyAssignment[workerIndex, taskIndex, week];
                            }

                            #region TASK FINISHED THIS WEEK - CHANGE PHASE METADATA
                            if (calcTask.RemainingHours <= 0)
                            {
                                aTaskFinishedThisWeek = true;
                                nrOfTasksFinishedInThisIteration++;
                                if (AppProject.CalculationPeriod == AppProject.Period.Weekly)
                                {
                                    calcTask.EndDate = weekDate.AddDays(4);
                                    PhaseMdList[phase].StartDate = lastPhaseDate;
                                    PhaseMdList[phase].EndDate = weekDate.AddDays(4);
                                    PhaseMdList[phase].TasksFinished.Add(calcTask.Task);
                                    PhaseMdList[phase].EndWeek = week + 1;
                                }
                                else
                                {
                                    calcTask.EndDate = weekDate;
                                    PhaseMdList[phase].StartDate = lastPhaseDate;
                                    PhaseMdList[phase].EndDate = weekDate;
                                    PhaseMdList[phase].TasksFinished.Add(calcTask.Task);
                                    PhaseMdList[phase].EndWeek = week;
                                }
                            }
                            #endregion
                        }

                    }
                    #endregion


                    #region CREATE PHASE IF A TASK HAS FINISHED THIS WEEK
                    if (aTaskFinishedThisWeek == true)
                    {
                        CreatePhase(phase++, week, lastPhaseWeek);
                        lastPhaseWeek = week + 1;
                        lastPhaseDate = weekDate.AddDays(7);
                    }
                    #endregion
                    if (AppProject.CalculationPeriod == AppProject.Period.Weekly)
                        weekDate = weekDate.AddDays(7);
                    else
                        weekDate = weekDate.AddDays(1);
                }
                #region Logical Error in Defintion
                if (week == _maxWeeks)
                {
                    string problemTasks = "";
                    foreach (CalcTask calcTask in TaskList)
                    {
                        if (calcTask.RemainingHours > 0)
                            problemTasks += "\n" + calcTask.Task.Name;
                    }

                    throw (new System.ApplicationException("There is a definition problem with tasks:" + problemTasks));
                }
                #endregion

                Phases = phase;
                #endregion


                #region ERROR CALCULATION
                int error = 0;
                foreach (CalcTask calcTask in TaskList)
                {
                    if (calcTask.FinishWithCalcTask != null)
                    {
                        long tickDifference = calcTask.EndDate.Date.Ticks - calcTask.FinishWithCalcTask.EndDate.Ticks;
                        tickDifference /= (10 * 1000 * 60 * 60 * 24); // days...100 nano seconds is on tick
                        tickDifference /= 1000;
                        // one week of error is okay with finishwithtask
                        //if ((tickDifference <= 7) && (tickDifference >= -7))
                        //    tickDifference = 0;
                        error += (int)(tickDifference * tickDifference);

                        #region calc new priority for task
                        long errorAdd = tickDifference;


                        calcTask.Error = (int)Math.Abs(tickDifference);

                        if (tickDifference < 0) errorAdd = -1;
                        else errorAdd = 1;
                        calcTask.Priority += (int)errorAdd;

                        #endregion

                    }
                    else if (calcTask.EndDate.Date > calcTask.Task.ExpectedEndDate.DateTime.Date)
                    {
                        long tickDifference = calcTask.EndDate.Date.Ticks - calcTask.Task.ExpectedEndDate.DateTime.Date.Ticks;
                        tickDifference /= (10 * 1000 * 60 * 60 * 24); // days...100 nano seconds is on tick
                        tickDifference /= 1000;
                        error += (int)(tickDifference * tickDifference);

                        #region calc new priority for task
                        long errorAdd = tickDifference;


                        calcTask.Error = (int)tickDifference;

                        if (errorAdd < 0) errorAdd = 0;
                        else errorAdd = 1;
                        calcTask.Priority += (int)errorAdd;

                        #endregion
                    }
                    else
                    {
                        calcTask.Error = 0;
                    }
                    if (calcTask.Priority < 10) calcTask.Priority = 10;
                }
                #endregion

                #region BEST ERROR
                if (error < BestError)
                {
                    BestError = error;
                    BestResult.PhaseAssignment = (int[, ,])PhaseAssignment.Clone();
                    BestResult.WeeklyAssignment = (int[, ,])WeeklyAssignment.Clone();
                    BestResult.PhaseMdList = (PhaseMetadata[])PhaseMdList.Clone();
                    BestResult.Phases = Phases;
                    BestResult.LastWeekWorking = lastWeekWorking;


                    foreach (CalcTask calcTask in TaskList)
                    {
                        calcTask.BestError = calcTask.Error;
                        calcTask.BestEndDate = calcTask.EndDate;
                    }

                    _backgroundWorker.ReportProgress(50);

                }

                #endregion



            }
            #endregion ITERATIONS
        }




        /// <summary>
        /// This method creates CalculationTasks out of Tasks and 
        /// CalculationWorkers out of Workers. A Calculation (model) object holds
        /// a reference to the data object and some other information necessary during
        /// the calculation. By doing so it is also easier to serialize the data objects
        /// </summary>
        private void Init(FolderTree taskFolderTree, FolderTree workerFolderTree)
        {
            #region Tasks to CalculationTasks
            TaskList.Clear();
            var tList = new List<Element>();
            taskFolderTree.CreateElementList(taskFolderTree.Folder, tList);
            foreach (Task task in tList)
            {
                if (task.Active)
                {
                    if (task.RemainingHours > 0)
                    {
                        CalcTask ct = new CalcTask();
                        ct.Task = task;
                        ct.Priority = 100;
                        TaskList.Add(ct);
                    }
                }
            }

            foreach (CalcTask calcTask in TaskList)
            {
                Task task = calcTask.Task;

                #region FINISHWITHCALCTASK
                foreach (CalcTask calcTask2 in TaskList)
                {
                    if (calcTask2.Task.Id == task.FinishWithTaskId)
                        calcTask.FinishWithCalcTask = calcTask2;
                }

                #endregion


                #region CALCPRDECESSORS
                foreach (Predecessor predecessor in task.PredecessorList)
                {
                    CalcPredecessor cp = new CalcPredecessor();
                    cp.Predecessor = predecessor;
                    foreach (CalcTask calcTask2 in TaskList)
                    {
                        if (calcTask2.Task.Id == predecessor.TaskId)
                            cp.CalcTask = calcTask2;
                    }


                    calcTask.CalcPredecessorList.Add(cp);

                }
                #endregion

                #region CALCSKILLEDWORKERS
                foreach (SkilledWorker skilledWorker in task.SkilledWorkerList)
                {
                    CalcSkilledWorker csw = new CalcSkilledWorker();
                    csw.SkilledWorker = skilledWorker;
                    csw.CalcTask = calcTask;
                    csw.HoursWorkedOnTask = 0;
                    calcTask.CalcSkilledWorkerList.Add(csw);
                }
                #endregion

            }
            #endregion

            #region Workers to CalculationWorkers
            WorkerList.Clear();
            var wList = new List<Element>();
            workerFolderTree.CreateElementList(workerFolderTree.Folder, wList);
            foreach (Worker worker in wList)
            {
                CalcWorker cw = new CalcWorker();
                cw.Worker = worker;
                WorkerList.Add(cw);
            }
            #endregion
        }

        private void InitIteration()
        {
            for (int x = 0; x < WorkerList.Count; x++)
                for (int y = 0; y < TaskList.Count; y++)
                    for (int z = 0; z < _maxWeeks; z++)
                        WeeklyAssignment[x, y, z] = 0;

            for (int x = 0; x < WorkerList.Count; x++)
                for (int y = 0; y < TaskList.Count + 1; y++)
                    for (int z = 0; z < TaskList.Count; z++)
                        PhaseAssignment[x, y, z] = 0;

            foreach (CalcTask calcTask in TaskList)
            {
                //calcTask.remainingHours = calcTask.task.EstimatedHours;
                calcTask.RemainingHours = calcTask.Task.RemainingHours;

                foreach (CalcSkilledWorker calcSkilledWorker in calcTask.CalcSkilledWorkerList)
                    calcSkilledWorker.HoursWorkedOnTask = 0;

            }

            for (int i = 0; i < TaskList.Count; i++)
                PhaseMdList[i] = new PhaseMetadata();
        }




        private int ChooseTask()
        {

            int[] probability = new int[50];

            for (int i = 0; i < TaskList.Count; i++)
            {
                if (_alreadyChosen.Contains(i) == true)
                    probability[i] = 0;
                else
                    probability[i] = (_random.Next(0, 100)) * ((CalcTask)TaskList[i]).Priority;

            }

            int highestProject = -1;
            int highestProbability = 0;
            for (int i = 0; i < TaskList.Count; i++)
            {
                //int test=taskList.taskList[i].calcEstimatedHours;
                if (probability[i] > highestProbability)
                {
                    highestProbability = probability[i];
                    highestProject = i;
                }
            }


            return highestProject;





        }




        private void CalculateDailyHoursPerWorker()
        {
            DateTime date = AppProject.StartDate.DateTime;
            for (int dayIndex = 0; dayIndex < _maxWeeks; dayIndex++)
            {
                for (int workerIndex = 0; workerIndex < WorkerList.Count; workerIndex++)
                {
                    Worker worker = ((CalcWorker)WorkerList[workerIndex]).Worker;
                    float hours4Worker4Day = worker.HoursPerWeek / 5.0f;



                    // do not work on weekend
                    if ((date.DayOfWeek == DayOfWeek.Saturday) || (date.DayOfWeek == DayOfWeek.Sunday))
                        hours4Worker4Day = 0;
                    else
                    {
                        // go through public holidays
                        foreach (Utilities.CalenderDate publicHoliday in AppProject.PublicHolidayList)
                        {
                            if (publicHoliday.DateTime == date)
                            {
                                hours4Worker4Day = 0;
                            }
                        }
                        // go through absent days
                        foreach (Utilities.CalenderDate holiday in worker.HolidayList)
                        {
                            if (holiday.DateTime == date)
                            {
                                hours4Worker4Day = 0;
                            }
                        }
                    }

                    WeeklyAssignment[workerIndex, TaskList.Count, dayIndex] = (int)hours4Worker4Day;

                }
                date = date.AddDays(1);

            }
        }

        private void CalculateWeeklyHoursPerWorker()
        {
            DateTime startDate4Week = AppProject.StartDate.DateTime;
            DateTime endDate4Week = startDate4Week.AddDays(5);

            for (int weekIndex = 0; weekIndex < _maxWeeks; weekIndex++)
            {
                for (int workerIndex = 0; workerIndex < WorkerList.Count; workerIndex++)
                {
                    Worker worker = ((CalcWorker)WorkerList[workerIndex]).Worker;
                    float hours4Worker4Week = worker.HoursPerWeek;
                    float hours4Worker4Day = hours4Worker4Week / 5.0f;

                    // go through public holidays

                    foreach (Utilities.CalenderDate publicHoliday in AppProject.PublicHolidayList)
                    {
                        if ((publicHoliday.DateTime >= startDate4Week) && (publicHoliday.DateTime <= endDate4Week))
                        {
                            hours4Worker4Week -= hours4Worker4Day;
                        }
                    }
                    // go through absent days
                    foreach (Utilities.CalenderDate holiday in worker.HolidayList)
                    {
                        if ((holiday.DateTime >= startDate4Week) && (holiday.DateTime <= endDate4Week))
                        {
                            hours4Worker4Week -= hours4Worker4Day;
                        }
                    }

                    if (hours4Worker4Week < 0) hours4Worker4Week = 0;
                    WeeklyAssignment[workerIndex, TaskList.Count, weekIndex] = (int)hours4Worker4Week;

                }
                startDate4Week = startDate4Week.AddDays(7);
                endDate4Week = endDate4Week.AddDays(7);
            }
        }

        /// 
        /// <param name="phase"></param>
        /// <param name="weekTo"></param>
        /// <param name="weekFrom"></param>
        private void CreatePhase(int phase, int weekTo, int weekFrom)
        {
            for (int workerIndex = 0; workerIndex < WorkerList.Count; workerIndex++)
            {

                for (int taskIndex = 0; taskIndex < TaskList.Count + 1; taskIndex++)
                {
                    //int workerHoursOnTaskPerPhase = 0;
                    for (int week = weekFrom; week <= weekTo; week++)
                    {
                        PhaseAssignment[workerIndex, taskIndex, phase] += WeeklyAssignment[workerIndex, taskIndex, week];
                    }

                }
            }
        }
    }//end Calculation
}



































































